// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package session

import (
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"sync"

	"chainmaker.org/chainmaker/smarteditor/docker"
	"github.com/88250/gulu"
)

type procs map[string][]*os.Process

// Processes of all users.
var Processes = procs{}

// Exclusive lock.
var procMutex sync.Mutex

var containerManager = docker.NewContainerManager(context.Background())

// StopHandler handles request of stopping a running process.
func StopHandler(w http.ResponseWriter, r *http.Request) {
	result := gulu.Ret.NewResult()
	defer gulu.Ret.RetResult(w, r, result)

	var args map[string]interface{}
	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
		log.Error(err)
		result.Code = -1
		return
	}

	sid := args["sid"].(string)
	pid := int(args["pid"].(float64))

	wSession := WideSessions.Get(sid)
	if nil == wSession {
		result.Code = -1
		return
	}
	Processes.Kill(wSession, pid)
}

// GetInvokeMethod handler request of getting contract method.
func GetInvokeMethodHandler(w http.ResponseWriter, r *http.Request) {
	result := gulu.Ret.NewResult()
	defer gulu.Ret.RetResult(w, r, result)

	var args map[string]interface{}

	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
		log.Error(err.Error())
		result.Code = -1
		result.Msg = err.Error()
		return
	}
	type resultData struct {
		Path    string   `json:"path"`
		Methods []string `json:"methods"`
	}

	resData := resultData{
		Path:    "",
		Methods: nil,
	}
	path := args["path"].(string)
	contractName := filepath.Base(path)
	var methods []string
	err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			log.Error(err.Error())
			return err
		}
		if info.IsDir() || len(methods) > 0 || !isGoFile(path) {
			return nil
		}
		if info.Name() == "main.go" {
			if resData.Path == "" {
				resData.Path = path
			}
		}
		err, meds := getMethodsFromFile(path)
		if err != nil {
			log.Error(err.Error())
			return err
		}
		if len(meds) > 0 {
			methods = append(methods, meds...)
			resData.Methods = methods
		}
		return nil
	})
	if err != nil {
		log.Error(err.Error())
		result.Code = -1
		result.Msg = err.Error()
		return
	}
	if resData.Path == "" {
		log.Error("contract path has no main.go file")
		result.Code = -1
		result.Msg = fmt.Sprintf("合约[%s]缺少main.go文件,合约调试功能无法正常使用", contractName)
		return
	}
	bytes, err := json.Marshal(resData)
	if err != nil {
		log.Error(err.Error())
		result.Code = -1
		result.Msg = err.Error()
		return
	}
	result.Data = string(bytes)

}
func isGoFile(path string) bool {
	if len(path) < 3 {
		return false
	}
	suffix := path[len(path)-3:]
	if string(suffix) == ".go" {
		return true
	}
	return false
}

//GetMethodsFormPath get method from path
func GetMethodsFormPath(path string) ([]string, error) {
	methods := make([]string, 0)
	err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
		if info.IsDir() || len(methods) > 0 || !isGoFile(path) {
			return nil
		}
		err, meds := getMethodsFromFile(path)
		if err != nil {
			log.Error(err.Error())
			return err
		}
		if len(meds) > 0 {
			methods = append(methods, meds...)
		}
		return nil
	})
	return methods, err
}

func getMethodsFromFile(path string) (error, []string) {
	f, err := ioutil.ReadFile(path)
	if err != nil {
		log.Errorf("read fail", err.Error())
		return err, nil
	}
	//remove code annotation
	code := string(f)
	code = removeAnnotation(code)
	if code == "" {
		return nil, []string{}
	}
	methods := getMethodsFromCode(code)
	return err, methods

}
func removeAnnotation(source string) string {
	// /**/
	r, err := regexp.Compile("/\\*{1,2}[\\s\\S]*?\\*/")
	if err != nil {
		log.Error(err.Error())
		return source
	}

	findStrArr := r.FindAllString(source, -1)
	//replace
	for i := 0; i < len(findStrArr); i++ {
		source = strings.ReplaceAll(source, findStrArr[i], "")
	}
	// //
	r, err = regexp.Compile("//[\\s\\S]*?\\n")
	if err != nil {
		log.Error(err.Error())
		return source
	}

	findStrArr = r.FindAllString(source, -1)
	for i := 0; i < len(findStrArr); i++ {
		source = strings.ReplaceAll(source, findStrArr[i], "")
	}
	return source

}

func getMethodsFromCode(code string) []string {
	var methods []string
	r, err := regexp.Compile("InvokeContract([\\d\\D]*)switch([\\d\\D]*)case([\\d\\D]*)default")
	if err != nil {
		log.Error(err.Error())
		return nil
	}
	code = r.FindString(code)
	r, err = regexp.Compile("case[\\s+](.*)\":")
	if err != nil {
		log.Error(err.Error())
		return nil
	}
	cases := r.FindAllString(code, -1)

	for _, method := range cases {
		method = getMethodFromCaseStr(method)
		methods = append(methods, method)
	}
	return methods
}
func getMethodFromCaseStr(caseStr string) string {
	if caseStr == "" {
		return ""
	}
	start := strings.Index(caseStr, "\"") + 1
	end := len(caseStr) - 2
	if start > end {
		return ""
	}
	return caseStr[start:end]
}

// Add adds the specified process to the user process set.
func (procs *procs) Add(wSession *WideSession, proc *os.Process) {
	procMutex.Lock()
	defer procMutex.Unlock()

	sid := wSession.ID
	userProcesses := (*procs)[sid]

	userProcesses = append(userProcesses, proc)
	(*procs)[sid] = userProcesses

	// bind process with wide session
	wSession.SetProcesses(userProcesses)

	log.Debugf("Session [%s] has [%d] processes", sid, len((*procs)[sid]))
}

// Remove removes the specified process from the user process set.
func (procs *procs) Remove(wSession *WideSession, proc *os.Process) {
	procMutex.Lock()
	defer procMutex.Unlock()

	sid := wSession.ID

	userProcesses := (*procs)[sid]

	var newProcesses []*os.Process
	for i, p := range userProcesses {
		if p.Pid == proc.Pid {
			newProcesses = append(userProcesses[:i], userProcesses[i+1:]...) // remove it
			(*procs)[sid] = newProcesses

			// bind process with wide session
			wSession.SetProcesses(newProcesses)

			log.Debugf("Session [%s] has [%d] processes", sid, len((*procs)[sid]))

			return
		}
	}
}

// Kill kills a process specified by the given pid.
func (procs *procs) Kill(wSession *WideSession, pid int) {
	procMutex.Lock()
	defer procMutex.Unlock()

	sid := wSession.ID

	userProcesses := (*procs)[sid]

	for i, p := range userProcesses {
		if p.Pid == pid {
			if err := p.Kill(); nil != err {
				log.Errorf("Kill a process [pid=%d] of user [%s, %s] failed [error=%v]",
					pid, wSession.UserID, sid, err)
			} else {
				var newProcesses []*os.Process

				newProcesses = append(userProcesses[:i], userProcesses[i+1:]...)
				(*procs)[sid] = newProcesses

				// bind process with wide session
				wSession.SetProcesses(newProcesses)

				log.Debugf("Killed a process [pid=%d] of user [%s, %s]",
					pid, wSession.UserID, sid)
			}

			return
		}
	}
}

func InitFailedDockerContainerReap() {
	go func() {
		containerManager.RemoveUnReleaseContainerLoop()
	}()
}
